"""
Copyright (c) 2009, ARIES Laboratory, University of Saskatchewan
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
    * Neither the name of the ARIES Laboratory, University of Saskatchewan nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

import threading
import time
import string
import os
from random import Random
from xml.dom.minidom import parse, parseString
from multiprocessing.managers import BaseManager
from Queue import Queue

class WorkManagerServer( BaseManager ):
    """
    Controller that handles passing out work packages.  Clients communicate
    with this server using the WorkManagerClient interfaces if they are
    connecting over a network, or via direct method invocation if they are
    within the same process space.
    """
    
    #todo: turn this into a priority queue
    #todo: make private
    work_packages_queue = Queue() # (packageid,package) tuples
    in_progress = dict() # Package indexed by packageid
    __result_handler = None
    __intermediate_result_handler = None
    __exception_handler = None
    __client_timeout = 30.0 # Timeout in seconds
    __default_port = 5012 # Default server port
    __default_authkey = "default" # Default key for Remote Manager
    
    def __init__(self, 
                 result_handler = None, 
                 intermediate_result_handler = None, 
                 exception_result_handler = None, 
                 port = __port_default, 
                 key = __default_authkey ):
        """
        When final results are submitted by client machines they are passed to
        the function identified by result_handler.  Intermediate results can
        be obtained by providing an intermediate_result_handler function.  The
        server is available for calling immediately after initialisation.
        Functions available to remote callers are:
          add_work_package
          get_work_package
          emit_result
          emit_intermediate_result
          processing_exception
          log
        """
        BaseManager.__init__(self, address=('',  port), authkey=key)
        self.register('add_work_package', callable=self.add_work_package)
        self.register('get_work_package', callable=self.get_work_package)
        self.register('emit_result', callable=self.emit_result)
        self.register('emit_intermediate_result', callable=self.emit_intermediate_result)
        self.register('processing_exception', callable=self.processing_exception)
        self.register('log', callable=self.log)

        self.__result_handler = result_handler 
        self.__intermediate_result_handler  = intermediate_result_handler

        self.start()
    
    def __random_id(self):
        """
        Generates a random string of characters and digits.
        """
        return "".join( Random().sample(string.letters + string.digits, 20))

    def add_work_package(self , package, packageid=__random_id(None)):
        """
        Adds a piece of work to the queue of work to be done.  The package can
        be of any type, and package ids are automatically generated if not
        provided.
        """
        #todo: what is a reasonable response if the package exists?
        self.work_packages_queue.put((packageid, package))
        return packageid

    def get_work_package( self ):
        """
        Returns a piece of work to do, removing it from the work queue and putting
        it in a temporary processing queue.  If results are not returned via the
        emit_result() function within a prescribed timeout this package will be
        reentered into the processing queue.
        Returns a tuple of (id,work)
        """
        #todo: do intermediate processing queue on timeout
        if self.work_packages_queue.qsize() > 0:
            package = self.work_packages_queue.get()
            # set timer for restart if the package isn't returned
            timer = Timer(self.__client_timeout, self.__readd_package, kwargs = {packageid:package[0]})
            timer.start()
            in_progress[package[0]]=(package[1], timer)

            return self.work_packages_queue.get()
        #todo: if this is not none it is a tuple, handle non-tuple on client side
        return None
    
    def __readd_package(self,packageid):
        """
        Removes the package from the in_progress queue and puts it in the
        work_packages_queue.
        """
        work, timer = in_progress[packageid]
        timer.cancel()
        del(in_progress[packageid])
        #todo: stick it in the front of the queue instead of just readding it
        add_work_package(work, packageid)

    def emit_result( self, packageid , result ):
        """
        Concludes the processing of the given package and invokes the
        result handler for this package.
        """
        if packageid in in_progress:
            timer = in_progress[packageid][1]
            timer.cancel()
            del(in_progress[packageid])
            if self.__result_handler != None:
                self.__result_handler( packageid , result )

    def emit_intermediate_result( self, packageid , result ):
        """
        Provides intermediate results for package.
        """
        if packageid in in_progress:
            if self.__intermediate_result_handler != None:
                self.__intermediate_result_handler ( packageid , result )
    
    def log( self , packageid, message ):
        """
        Traceable logging functionality.
        """
        #todo: make this more traditional logging
        print message
        
    def processing_exception(self, packageid, exception):
        """
        Removes a package from the progress queue and invokes the exception
        handler if provided
        """
        if packageid in in_progress:
            work, timer = in_progress[packageid]
            timer.cancel()
            del(in_progress[packageid])
            if self.__exception_handler != None:
                self.__exception_handler ( packageid , exception )

class WorkManagerClient:
    """
    Stub that communicates with remote managers to receive and report on
    the progress of distributed jobs.
    """
    
    #todo: make some of these private
    manager = None # Underlying BaseManager class for remote communication
    working = False
    timer = None
    worker_function = None
    wait_time = 5.0
    packageid = None
    __default_port = 5012 # Default server port
    __default_authkey = "default" # Default key for Remote Manager
    __default_host = localhost # Default server to connect to
    
    def __init__(self, 
                 worker_function, 
                 ip = __default_host, 
                 port = __default_port, 
                 key = __default_authkey, 
                 worker_wait_time = wait_time):
        """
        Registers initial functions for callers
        Functions available to remote callers are:
          add_work_package
          get_work_package
          emit_result
          emit_intermediate_result
          processing_exception
          log
        """
        
        # The local function to send work packages to
        self.worker_function = worker_function
        self.wait_time = worker_wait_time
        
        # Stub
        class RemoteManager(BaseManager):
            pass
    
        RemoteManager.register('add_work_package')
        RemoteManager.register('get_work_package')
        RemoteManager.register('emit_result')
        RemoteManager.register('emit_intermediate_result')
        RemoteManager.register('processing_exception')
        RemoteManager.register('log')
    
        self.manager = RemoteManager(address=(ip, port), authkey=key)
        self.manager.connect()
    
        if self.wait_time > 0:
            self.timer = threading.Timer( self.wait_time, self.check_for_work )
            self.timer.start()

    #todo Make private
    def check_for_work(self):
        """
        Checks for work automatically for new work packages on the server.
        """
        if (not(self.working)):
            self.working = True
            self.packageid, work = self.manager.get_work_package()._getvalue()
            if work != None:
                result = self.worker_function(self.packageid, work)
                self.manager.emit_result(self.packageid, result)
                
        self.working = False
        self.timer = threading.Timer(self.wait_time, self.check_for_work)
        self.timer.start()
        
    def log(self, message):
        self.manager.log(self.packageid, message)
    
    def emit_intermediate_result(self, message):
        self.manager.emit_intermediate_result(self.packageid, message)

    def emit_result(self, message):
        self.manager.emit_result(self.packageid, message)

    def processing_exception(self, exception):
        self.manager.processing_exception(self.packageid, exception)

    def add_work_package(self, package, packageid=None):
        if packageid==None:
            self.manager.add_work_package(package)
        else:
            self.manager.add_work_package(package, packageid)

